﻿/*
 Copyright aswing.org, see the LICENCE.txt.
*/
import org.aswing.event.TreeModelEvent;
import org.aswing.geom.Rectangle;
import org.aswing.tree.AbstractLayoutCache;
import org.aswing.tree.EnumerationInfo;
import org.aswing.tree.FHTreeStateNode;
import org.aswing.tree.SearchInfo;
import org.aswing.tree.TreeModel;
import org.aswing.tree.TreePath;
import org.aswing.tree.TreePathMap;
import org.aswing.util.Stack;

/**
 * @author iiley
 */
class org.aswing.tree.FixedHeightLayoutCache extends AbstractLayoutCache{
    /** Root node. */
    private var root:FHTreeStateNode;

    /** Number of rows currently visible. */
    private var rowCount:Number;

    /**
     * Used in getting sizes for nodes to avoid creating a new Rectangle
     * every time a size is needed.
     */
    private var boundsBuffer:Rectangle;

    /**
     * Maps from TreePath to a FHTreeStateNode.
     */
    private var treePathMapping:TreePathMap;

    /**
     * Used for getting path/row information.
     */
    private var info:SearchInfo;

    private var tempStacks:Stack;


    public function FixedHeightLayoutCache() {
		super();
		rowCount = 0;
		rootVisible = false;
		tempStacks = new Stack();
		boundsBuffer = new Rectangle();
		treePathMapping = new TreePathMap();
		info = new SearchInfo(this);
		setRowHeight(16);
    }

    /**
     * Sets the TreeModel that will provide the data.
     *
     * @param newModel the TreeModel that is to provide the data
     */
    public function setModel(newModel:TreeModel):Void {
		super.setModel(newModel);
		rebuild(false);
    }

    /**
     * Determines whether or not the root node from
     * the TreeModel is visible.
     *
     * @param rootVisible true if the root node of the tree is to be displayed
     * @see #rootVisible
     */
    public function setRootVisible(rootVisible:Boolean):Void {
		if(isRootVisible() != rootVisible) {
		    super.setRootVisible(rootVisible);
		    if(root != null) {
				if(rootVisible) {
				    rowCount++;
				    root.adjustRowBy(1);
				}else {
				    rowCount--;
				    root.adjustRowBy(-1);
				}
				visibleNodesChanged();
		    }
		}
    }

    /**
     * Sets the height of each cell. If rowHeight is less than or equal to
     * 0 this will throw an IllegalArgumentException.
     *
     * @param rowHeight the height of each cell, in pixels
     */
    public function setRowHeight(rowHeight:Number):Void {
		if(rowHeight <= 0){
			trace("Error : FixedHeightLayoutCache only supports row heights greater than 0");
			throw new Error("FixedHeightLayoutCache only supports row heights greater than 0");
		}
		if(getRowHeight() != rowHeight) {
		    super.setRowHeight(rowHeight);
		    visibleNodesChanged();
		}
    }
	
	public function setRowCount(rc:Number):Void{
		rowCount = rc;
	}
	
    /**
     * Returns the number of visible rows.
     */
    public function getRowCount():Number {
		return rowCount;
    }

    /**
     * Does nothing, FixedHeightLayoutCache doesn't cache width, and that
     * is all that could change.
     */
    public function invalidatePathBounds(path:TreePath):Void {
    }


    /**
     * Informs the TreeState that it needs to recalculate all the sizes
     * it is referencing.
     */
    public function invalidateSizes():Void {
		// Nothing to do here, rowHeight still same, which is all
		// this is interested in, visible region may have changed though.
		visibleNodesChanged();
    }

    /**
      * Returns true if the value identified by row is currently expanded.
      */
    public function isExpanded(path:TreePath):Boolean {
		if(path != null) {
		    var lastNode:FHTreeStateNode = getNodeForPath(path, true, false);
		    return (lastNode != null && lastNode.isExpanded());
		}
		return false;
    }

    /**
     * Returns a rectangle giving the bounds needed to draw path.
     *
     * @param path     a TreePath specifying a node
     * @param placeIn  a Rectangle object giving the available space, to avoid create new instance.
     * @return a Rectangle object specifying the space to be used
     */
    public function getBounds(path:TreePath, placeIn:Rectangle):Rectangle {
		if(path == null)
		    return null;
	
		var node:FHTreeStateNode = getNodeForPath(path, true, false);
	
		if(node != null)
		    return getBounds2(node, -1, placeIn);
	
		// node hasn't been created yet.
		var parentPath:TreePath = path.getParentPath();
		node = getNodeForPath(parentPath, true, false);
		if(node != null) {
		    var childIndex:Number = treeModel.getIndexOfChild(parentPath.getLastPathComponent(),
					  path.getLastPathComponent());
			//trace("childIndex = " + childIndex);
		    if(childIndex != -1){
				return getBounds2(node, childIndex, placeIn);
		    }
		}
		return null;
    }

    /**
      * Returns the path for passed in row.  If row is not visible
      * null is returned.
      */
    public function getPathForRow(row:Number):TreePath {
		if(row >= 0 && row < getRowCount()) {
		    if(root.getPathForRow(row, getRowCount(), info)) {
				return info.getPath();
		    }
		}
		return null;
    }

    /**
      * Returns the row that the last item identified in path is visible
      * at.  Will return -1 if any of the elements in path are not
      * currently visible.
      */
    public function getRowForPath(path:TreePath):Number {
		if(path == null || root == null)
		    return -1;
	
		var node:FHTreeStateNode = getNodeForPath(path, true, false);
	
		if(node != null)
		    return node.getRow();
	
		var parentPath:TreePath = path.getParentPath();
	
		node = getNodeForPath(parentPath, true, false);
		if(node != null && node.isExpanded()) {
		    return node.getRowToModelIndex(treeModel.getIndexOfChild(parentPath.getLastPathComponent(),
						    path.getLastPathComponent()));
		}
		return -1;
    }

    /**
      * Returns the path to the node that is closest to x,y.  If
      * there is nothing currently visible this will return null, otherwise
      * it'll always return a valid path.  If you need to test if the
      * returned object is exactly at x, y you should get the bounds for
      * the returned path and test x, y against that.
      */
    public function getPathClosestTo(x:Number, y:Number):TreePath {
		if(getRowCount() == 0)
		    return null;
	
		var row:Number = getRowContainingYLocation(y);
	
		return getPathForRow(row);
    }

    /**
     * Returns the number of visible children for row.
     */
    public function getVisibleChildCount(path:TreePath):Number {
		var node:FHTreeStateNode = getNodeForPath(path, true, false);
	
		if(node == null)
		    return 0;
		return node.getTotalChildCount();
    }

    /**
     * Returns an array that increments over the visible paths
     * starting at the passed in location and end by totalCount of the tree end. 
     * The ordering of the enumeration is based on how the paths are displayed.
     * @param totalCount the total number of path to contains.
     */
    public function getVisiblePathsFrom(path:TreePath, totalCount:Number):Array {
		if(path == null)
		    return null;
		    
		if(totalCount == undefined){
			trace("Error : totalCount is undefined!!");
			return null;
		}
	
		var node:FHTreeStateNode = getNodeForPath(path, true, false);
	
		if(node != null) {
		    return getVisibleFHTreeStateNodes(node, -1, totalCount);
		}
		var parentPath:TreePath = path.getParentPath();
	
		node = getNodeForPath(parentPath, true, false);
		if(node != null && node.isExpanded()) {
		    return getVisibleFHTreeStateNodes(
		    	node, 
		    	treeModel.getIndexOfChild(
		    		parentPath.getLastPathComponent(), 
		    		path.getLastPathComponent()),
		    	totalCount);
		}
		return null;
    }
    
    private function nextVisibleFHTreeStateNode(info:EnumerationInfo):TreePath{    	
    	var nextIndex:Number = info.nextIndex;
    	var childCount:Number = info.childCount;
    	var parent:FHTreeStateNode = info.parent;
    	var retObject:TreePath;
		if(nextIndex == -1){
    		retObject = parent.getTreePath();
    	}else{
			var  node:FHTreeStateNode = parent.getChildAtModelIndex(nextIndex);
			if(node == null){
			    retObject = parent.getTreePath().pathByAddingChild(
			    				treeModel.getChild(parent.getUserObject(),
								nextIndex));
			}else{
			    retObject = node.getTreePath();
			}
    	}
    	updateNextObject(info);
    	info.enumCount--;
    	return retObject;
    }
    private function hasMoreVisibleFHTreeStateNode(info:EnumerationInfo):Boolean{
    	return info.enumCount > 0 && info.parent != null;
    }
    
    private function updateNextObject(info:EnumerationInfo):Void{
	    if(!updateNextIndex(info)) {
			findNextValidParent(info);
	    }
    }
    private function updateNextIndex(info:EnumerationInfo):Boolean{
	    if(info.nextIndex == -1 && !info.parent.isExpanded()) {
			return false;
	    }
	    if(info.childCount == 0) {// Check that it can have kids
			return false;
	    }
	    
	    info.nextIndex++;
	    
	    if(info.nextIndex >= info.childCount) {// Make sure next index not beyond child count.
			return false;
	    }
	    var child:FHTreeStateNode = info.parent.getChildAtModelIndex(info.nextIndex);

	    if(child != null && child.isExpanded()) {
			info.parent = child;
			info.nextIndex = -1;
			info.childCount = treeModel.getChildCount(child.getUserObject());
	    }
	    return true;
    }
    private function findNextValidParent(info:EnumerationInfo):Boolean{
	    if(info.parent == root) {
			// mark as invalid!
			info.parent = null;
			return false;
	    }
	    while(info.parent != null) {
			var newParent:FHTreeStateNode = FHTreeStateNode(info.parent.getParent());
	
			if(newParent != null) {
			    info.nextIndex = info.parent.getChildIndex();
			    info.parent = newParent;
			    info.childCount = treeModel.getChildCount(info.parent.getUserObject());
			    if(updateNextIndex(info))
					return true;
			}else
			    info.parent = null;
	    }
	    return false;
    }
    
    private function getVisibleFHTreeStateNodes(parent:FHTreeStateNode, startIndex:Number, totalCount:Number):Array{
    	var nodes:Array = new Array();
    	var info:EnumerationInfo = new EnumerationInfo();
    	info.parent = parent;
    	info.nextIndex = startIndex;
    	info.childCount = treeModel.getChildCount(parent.getUserObject());
    	info.enumCount = totalCount;
    	while(hasMoreVisibleFHTreeStateNode(info)){
    		nodes.push(nextVisibleFHTreeStateNode(info));
    	}
    	return nodes;
    }

    /**
     * Marks the path <code>path</code> expanded state to
     * <code>isExpanded</code>.
     */
    public function setExpandedState(path:TreePath, isExpanded:Boolean):Void {
		if(isExpanded){
		    ensurePathIsExpanded(path, true);
		}else if(path != null) {
		    var parentPath:TreePath = path.getParentPath();
	
		    // YECK! Make the parent expanded.
		    if(parentPath != null) {
				var parentNode:FHTreeStateNode = getNodeForPath(parentPath, false, true);
				if(parentNode != null)
				    parentNode.makeVisible();
		    }
		    // And collapse the child.
		    var childNode:FHTreeStateNode = getNodeForPath(path, true, false);
	
		    if(childNode != null){
				childNode.collapse(true);
		    }
		}
    }

    /**
     * Returns true if the path is expanded, and visible.
     */
    public function getExpandedState(path:TreePath):Boolean {
		var node:FHTreeStateNode = getNodeForPath(path, true, false);
	
		return (node != null) ? (node.isVisible() && node.isExpanded()) : false;
    }

    //
    // TreeModelListener methods
    //

    /**
     * <p>Invoked after a node (or a set of siblings) has changed in some
     * way. The node(s) have not changed locations in the tree or
     * altered their children arrays, but other attributes have
     * changed and may affect presentation. Example: the name of a
     * file has changed, but it is in the same location in the file
     * system.</p>
     *
     * <p>e.path() returns the path the parent of the changed node(s).</p>
     *
     * <p>e.childIndices() returns the index(es) of the changed node(s).</p>
     */
    public function treeNodesChanged(e:TreeModelEvent):Void {
		if(e != null) {
		    var changedIndexs:Array;
		    var changedParent:FHTreeStateNode = getNodeForPath(e.getTreePath(), false, false);
		    var maxCounter:Number;
	
		    changedIndexs = e.getChildIndices();
		    /* Only need to update the children if the node has been
		       expanded once. */
		    // PENDING(scott): make sure childIndexs is sorted!
		    if (changedParent != null) {
				if (changedIndexs != null && (maxCounter = changedIndexs.length) > 0) {
				    var parentValue:Object = changedParent.getUserObject();
		
				    for(var counter:Number = 0; counter < maxCounter; counter++) {
						var child:FHTreeStateNode = changedParent.getChildAtModelIndex(changedIndexs[counter]);
			
						if(child != null) {
						    child.setUserObject(treeModel.getChild(parentValue, changedIndexs[counter]));
						}
				    }
				    if(changedParent.isVisible() && changedParent.isExpanded()){
						visibleNodesChanged();
				    }
				}
				// Null for root indicates it changed.
				else if (changedParent == root && changedParent.isVisible() &&
					 changedParent.isExpanded()) {
				    visibleNodesChanged();
				}
		    }
		}
    }

    /**
     * <p>Invoked after nodes have been inserted into the tree.</p>
     *
     * <p>e.path() returns the parent of the new nodes
     * <p>e.childIndices() returns the indices of the new nodes in
     * ascending order.
     */
    public function treeNodesInserted(e:TreeModelEvent):Void {
		if(e != null) {
		    var changedIndexs:Array;
		    var changedParent:FHTreeStateNode = getNodeForPath(e.getTreePath(), false, false);
		    var maxCounter:Number;
	
		    changedIndexs = e.getChildIndices();
		    /* Only need to update the children if the node has been
		       expanded once. */
		    // PENDING(scott): make sure childIndexs is sorted!
		    if(changedParent != null && changedIndexs != null && (maxCounter = changedIndexs.length) > 0) {
				var isVisible:Boolean = (changedParent.isVisible() && changedParent.isExpanded());
		
				for(var counter:Number = 0; counter < maxCounter; counter++) {
				    changedParent.childInsertedAtModelIndex(changedIndexs[counter], isVisible);
				}
				if(isVisible && treeSelectionModel != null)
				    treeSelectionModel.resetRowSelection();
				if(changedParent.isVisible())
				    visibleNodesChanged();
		    }
		}
    }

    /**
     * <p>Invoked after nodes have been removed from the tree.  Note that
     * if a subtree is removed from the tree, this method may only be
     * invoked once for the root of the removed subtree, not once for
     * each individual set of siblings removed.</p>
     *
     * <p>e.path() returns the former parent of the deleted nodes.</p>
     *
     * <p>e.childIndices() returns the indices the nodes had before they were deleted in ascending order.</p>
     */
    public function treeNodesRemoved(e:TreeModelEvent):Void {
		if(e != null) {
		    var changedIndexs:Array;
		    var maxCounter:Number;
		    var parentPath:TreePath = e.getTreePath();
		    var changedParentNode:FHTreeStateNode = getNodeForPath(parentPath, false, false);
	
		    changedIndexs = e.getChildIndices();
		    // PENDING(scott): make sure that changedIndexs are sorted in
		    // ascending order.
		    if(changedParentNode != null && changedIndexs != null && (maxCounter = changedIndexs.length) > 0) {
			var children:Array = e.getChildren();
			var isVisible:Boolean = (changedParentNode.isVisible() &&
			     changedParentNode.isExpanded());
	
			for(var counter:Number = maxCounter - 1; counter >= 0; counter--) {
			    changedParentNode.removeChildAtModelIndex(changedIndexs[counter], isVisible);
			}
			if(isVisible) {
			    if(treeSelectionModel != null){
					treeSelectionModel.resetRowSelection();
			    }
                if (treeModel.getChildCount(changedParentNode.getUserObject()) == 0 && changedParentNode.isLeaf()) {
                    // Node has become a leaf, collapse it.
                    changedParentNode.collapse(false);
                }
			    visibleNodesChanged();
			}else if(changedParentNode.isVisible())
			    visibleNodesChanged();
		    }
		}
    }

    /**
     * <p>Invoked after the tree has drastically changed structure from a
     * given node down.  If the path returned by e.getPath() is of length
     * one and the first element does not identify the current root node
     * the first element should become the new root of the tree.<p>
     *
     * <p>e.path() holds the path to the node.</p>
     * <p>e.childIndices() returns null.</p>
     */
    public function treeStructureChanged(e:TreeModelEvent):Void {
		if(e != null) {
		    var changedPath:TreePath = e.getTreePath();
	        var changedNode:FHTreeStateNode = getNodeForPath(changedPath, false, false);
	
		    // Check if root has changed, either to a null root, or
	            // to an entirely new root.
		    if (changedNode == root ||
	                (changedNode == null &&
	                 ((changedPath == null && treeModel != null &&
	                   treeModel.getRoot() == null) ||
	                  (changedPath != null && changedPath.getPathCount() <= 1)))) {
	                rebuild(true);
	            }else if(changedNode != null) {
	                var wasExpanded:Boolean, wasVisible:Boolean;
	                var parent:FHTreeStateNode = FHTreeStateNode(changedNode.getParent());
	
	                wasExpanded = changedNode.isExpanded();
	                wasVisible = changedNode.isVisible();
	
					var index:Number = parent.getIndex(changedNode);
					changedNode.collapse(false);
					parent.removeAt(index);
	
	                if(wasVisible && wasExpanded) {
						var row:Number = changedNode.getRow();
						parent.resetChildrenRowsFrom(row, index, changedNode.getChildIndex());
						changedNode = getNodeForPath(changedPath, false, true);
						changedNode.expand();
	                }
	                if(treeSelectionModel != null && wasVisible && wasExpanded){
						treeSelectionModel.resetRowSelection();
	                }
	                if(wasVisible){
						visibleNodesChanged();
	                }
		    }
		}
    }


    //
    // Local methods
    //

    private function visibleNodesChanged():Void {
    }

    /**
     * Returns the bounds for the given node. If <code>childIndex</code>
     * is -1, the bounds of <code>parent</code> are returned, otherwise
     * the bounds of the node at <code>childIndex</code> are returned.
     */
    private function getBounds2(parent:FHTreeStateNode, childIndex:Number, placeIn:Rectangle):Rectangle {
		var expanded:Boolean;
		var level:Number;
		var row:Number;
		var value:Object;
	
		if(childIndex == -1) {
		    // Getting bounds for parent
		    row = parent.getRow();
		    value = parent.getUserObject();
		    expanded = parent.isExpanded();
		    level = parent.getLevel();
		}else {
		    row = parent.getRowToModelIndex(childIndex);
		    value = treeModel.getChild(parent.getUserObject(), childIndex);
		    expanded = false;
		    level = parent.getLevel() + 1;
		}
	
		var bounds:Rectangle = countNodeDimensions(value, row, level, expanded, boundsBuffer);
		// No node dimensions, bail.
		if(bounds == null){
		    return null;
		}
		if(placeIn == null){
		    placeIn = new Rectangle();
		}
		placeIn.x = bounds.x;
		placeIn.height = getRowHeight();
		placeIn.y = row * placeIn.height;
		placeIn.width = bounds.width;
		return placeIn;
    }

    /**
     * Adjust the large row count of the AbstractTreeUI the receiver was
     * created with.
     */
    public function adjustRowCountBy(changeAmount:Number):Void {
		rowCount += changeAmount;
    }

    /**
     * Adds a mapping for node.
     */
    public function addMapping(node:FHTreeStateNode):Void {
		treePathMapping.put(node.getTreePath(), node);
    }

    /**
     * Removes the mapping for a previously added node.
     */
    public function removeMapping(node:FHTreeStateNode):Void {
		treePathMapping.remove(node.getTreePath());
    }

    /**
     * Returns the node previously added for <code>path</code>. This may
     * return null, if you to create a node use getNodeForPath.
     */
    private function getMapping(path:TreePath):FHTreeStateNode {
		return FHTreeStateNode(treePathMapping.get(path));
    }

    /**
     * Sent to completely rebuild the visible tree. All nodes are collapsed.
     */
    private function rebuild(clearSelection:Boolean):Void {
        var rootUO:Object;

		treePathMapping.clear();
		rootUO = treeModel.getRoot();
		//trace("rebuild rootUO : " + rootUO);
		if(treeModel != null && rootUO != null) {
		    root = createNodeForValue(rootUO, 0);
		    //trace("root : " + root);
		    root.setPath(new TreePath([rootUO]));
		    addMapping(root);
		    if(isRootVisible()) {
				rowCount = 1;
				root.setRow(0);
		    }else {
				rowCount = 0;
				root.setRow(-1);
		    }
		    root.expand();
		}else {
		    root = null;
		    rowCount = 0;
		}
		if(clearSelection && treeSelectionModel != null) {
		    treeSelectionModel.clearSelection();
		}
		this.visibleNodesChanged();
    }

    /**
      * Returns the index of the row containing location.  If there
      * are no rows, -1 is returned.  If location is beyond the last
      * row index, the last row index is returned.
      */
    public function getRowContainingYLocation(location:Number):Number {
		if(getRowCount() == 0){
		    return -1;
		}
		return Math.floor(Math.max(0, Math.min(getRowCount() - 1, location / getRowHeight())));
    }

    /**
     * Ensures that all the path components in path are expanded, accept
     * for the last component which will only be expanded if expandLast
     * is true.
     * Returns true if succesful in finding the path.
     */
    private function ensurePathIsExpanded(aPath:TreePath, expandLast:Boolean):Boolean {
		if(aPath != null) {
		    // Make sure the last entry isn't a leaf.
		    if(treeModel.isLeaf(aPath.getLastPathComponent())) {
				aPath = aPath.getParentPath();
				expandLast = true;
		    }
		    if(aPath != null) {
				var lastNode:FHTreeStateNode = getNodeForPath(aPath, false, true);
		
				if(lastNode != null) {
				    lastNode.makeVisible();
				    if(expandLast){
						lastNode.expand();
				    }
				    return true;
				}
		    }
		}
		return false;
    }

    /**
     * Creates and returns an instance of FHTreeStateNode.
     */
    public function createNodeForValue(value:Object, childIndex:Number):FHTreeStateNode {
		return new FHTreeStateNode(this, value, childIndex, -1);
    }

    /**
     * Messages getTreeNodeForPage(path, onlyIfVisible, shouldCreate,
     * path.length) as long as path is non-null and the length is > 0.
     * Otherwise returns null.
     */
    private function getNodeForPath(path:TreePath, onlyIfVisible:Boolean, shouldCreate:Boolean):FHTreeStateNode {
		if(path != null) {
		    var node:FHTreeStateNode;
	
		    node = getMapping(path);
		    if(node != null) {
				if(onlyIfVisible && !node.isVisible())
				    return null;
				return node;
		    }
		    if(onlyIfVisible)
				return null;
	
		    // Check all the parent paths, until a match is found.
		    var paths:Stack;
	
		    if(tempStacks.size() == 0) {
				paths = new Stack();
		    }
		    else {
				paths = Stack(tempStacks.pop());
		    }
	
		    try {
				paths.push(path);
				path = path.getParentPath();
				node = null;
				while(path != null) {
				    node = getMapping(path);
				    if(node != null) {
						// Found a match, create entries for all paths in
						// paths.
						while(node != null && paths.size() > 0) {
						    path = TreePath(paths.pop());
						    node = node.createChildFor(path.getLastPathComponent());
						}
						return node;
				    }
				    paths.push(path);
				    path = path.getParentPath();
				}
		    }catch(e:Error){
				paths.clear();
				tempStacks.push(paths);
		    }
		    // If we get here it means they share a different root!
		    return null;
		}
		return null;
    }	
}